/**
 * @Author: grainbuds
 * @Created Date: 2023年03月26日 下午2:54 星期日
 * @Description: 定制日志输出
 */
package log

import (
	"fmt"
	"github.com/fatih/color"
	"log"
	"os"
	"path"
	"path/filepath"
	"runtime"
	"strings"
	"time"
)

// levels
type logLevel int
type LogMode string

const (
	DebugLevel logLevel = iota
	TraceLevel
	AlertLevel
	ErrorLevel
	FatalLevel
	DebugMode LogMode = "[debug]"
	TraceMode LogMode = "[trace]"
	AlertMode LogMode = "[alert]"
	ErrorMode LogMode = "[error]"
	FatalMode LogMode = "[fatal]"
)

var (
	// 调用层级
	//defaultCallerDepth = 2

	// 日志关键字
	keywords = map[logLevel]string{
		DebugLevel: strings.ToUpper(string(DebugMode)),
		TraceLevel: strings.ToUpper(string(TraceMode)),
		AlertLevel: strings.ToUpper(string(AlertMode)),
		ErrorLevel: strings.ToUpper(string(ErrorMode)),
		FatalLevel: strings.ToUpper(string(FatalMode)),
	}
	// 日志等级
	keyLevels = map[LogMode]logLevel{
		DebugMode: DebugLevel,
		TraceMode: TraceLevel,
		AlertMode: AlertLevel,
		ErrorMode: ErrorLevel,
		FatalMode: FatalLevel,
	}
)

type Logger struct {
	super              *log.Logger
	fd                 *os.File
	level              logLevel
	cacheQueue         chan logData
	stopChan           chan struct{}
	useColor           bool
	defaultCallerDepth int
}

type logData struct {
	level logLevel
	datum string
}

func NewLog(logLevel LogMode, logPath string) *Logger {
	logLevel = "[" + logLevel + "]"
	if _, ok := keyLevels[logLevel]; !ok {
		logLevel = ErrorMode
	}
	CreateDir(logPath)

	self := new(Logger) // log struct
	self.level = keyLevels[logLevel]
	self.useColor = false
	self.defaultCallerDepth = 2

	if strings.Compare(logPath, "") == 0 { // console output
		self.fd = os.Stdout
		self.useColor = true
		self.super = log.New(self.fd, "", log.Lmsgprefix)
	} else {
		filenamePrefix := "debug"
		if self.level > DebugLevel {
			filenamePrefix = "release"
		}
		// file output
		self.fd, _ = os.OpenFile(path.Join(logPath, GetFileName(filenamePrefix)), os.O_RDWR|os.O_CREATE|os.O_APPEND, os.FileMode(0666))
		self.super = log.New(self.fd, "", log.Lmsgprefix)
	}
	return self
}

func NewAsyncLog(logLevel LogMode, logPath string) *Logger {
	log := NewLog(logLevel, logPath)
	log.stopChan = make(chan struct{})
	log.cacheQueue = make(chan logData, 512)
	go func() {
		defer func() {
			checkError()
			if log.cacheQueue != nil {
				close(log.cacheQueue)
			}
		}()

		for {
			select {
			case <-log.stopChan:
				log.Close()
				return
			case v := <-log.cacheQueue:
				log.doOutput(v.level, v.datum)
			}
		}
	}()
	return log
}

func defaultNewLog() (logger *Logger) {
	logger = NewLog("debug", "")
	logger.defaultCallerDepth = 3
	return
}

func (self *Logger) Close() {
	self.super = nil
	if self.fd != nil {
		self.fd.Close()
	}
}

func (self *Logger) formatLogPrefix(level logLevel) string {
	_, file, line, _ := runtime.Caller(self.defaultCallerDepth)
	if !strings.Contains(file, "engine") {
		pwd, _ := os.Getwd()
		file = file[len(pwd)+1:]
	} else {
		file = file[strings.Index(file, "engine"):]
	}
	return fmt.Sprintf("[%4s] %s [%s:%d] ", time.Now().Format(time.DateTime), self.withColor(level, keywords[level]), file, line)
}

func (self *Logger) doOutput(level logLevel, data string) {
	if level < self.level {
		return
	}
	if self.super == nil {
		panic("log closed")
	}

	self.super.Output(self.defaultCallerDepth, data)

	if level == FatalLevel {
		Stop()
		os.Exit(1)
	}
}

func (self *Logger) withColor(logLevel logLevel, data string) string {
	if self.useColor {
		switch logLevel {
		case DebugLevel:
			return color.GreenString(data)
		case TraceLevel:
			return color.CyanString(data)
		case AlertLevel:
			return color.YellowString(data)
		case ErrorLevel:
			return color.MagentaString(data)
		case FatalLevel:
			return color.RedString(data)
		}
	}
	return data
}

func (self *Logger) Debugf(format string, a ...interface{}) {
	data := self.formatLogPrefix(DebugLevel) + fmt.Sprintf(format, a...)
	if self.cacheQueue != nil {
		self.cacheQueue <- logData{DebugLevel, data}
	} else {
		self.doOutput(DebugLevel, data)
	}
}

func (self *Logger) Tracef(format string, a ...interface{}) {
	data := self.formatLogPrefix(TraceLevel) + self.withColor(TraceLevel, fmt.Sprintf(format, a...))
	if self.cacheQueue != nil {
		self.cacheQueue <- logData{TraceLevel, data}
	} else {
		self.doOutput(TraceLevel, data)
	}
}

func (self *Logger) Alertf(format string, a ...interface{}) {
	data := self.formatLogPrefix(AlertLevel) + self.withColor(AlertLevel, fmt.Sprintf(format, a...))
	if self.cacheQueue != nil {
		self.cacheQueue <- logData{AlertLevel, data}
	} else {
		self.doOutput(AlertLevel, data)
	}
}

func (self *Logger) Errorf(format string, a ...interface{}) {
	data := self.formatLogPrefix(ErrorLevel) + self.withColor(ErrorLevel, fmt.Sprintf(format, a...))
	if self.cacheQueue != nil {
		self.cacheQueue <- logData{ErrorLevel, data}
	} else {
		self.doOutput(ErrorLevel, data)
	}
}

func (self *Logger) Fatalf(format string, a ...interface{}) {
	data := self.formatLogPrefix(FatalLevel) + self.withColor(FatalLevel, fmt.Sprintf(format, a...))
	if self.cacheQueue != nil {
		self.cacheQueue <- logData{FatalLevel, data}
	} else {
		self.doOutput(FatalLevel, data)
	}
}

var gLogger = defaultNewLog()

// It's dangerous to call the method on logging
func Export(this *Logger) {
	if this != nil {
		gLogger = this
		gLogger.defaultCallerDepth = 3
	}
}

func Debug(format string, a ...interface{}) {
	gLogger.Debugf(format, a...)
}

func Trace(format string, a ...interface{}) {
	gLogger.Tracef(format, a...)
}

func Alert(format string, a ...interface{}) {
	gLogger.Alertf(format, a...)
}

func Error(format string, a ...interface{}) {
	gLogger.Errorf(format, a...)
}

func Fatal(format string, a ...interface{}) {
	gLogger.Fatalf(format, a...)
}

func Close() {
	gLogger.Close()
}

func Stop() {
	if gLogger.stopChan != nil {
		gLogger.stopChan <- struct{}{}
		close(gLogger.stopChan)
	}
}

// 创建目录
func CreateDir(path string) {
	if strings.Compare(path, "") == 0 {
		return
	}
	if filepath.Ext(path) != "" {
		path = filepath.Dir(path)
	}
	if _, err := os.Stat(path); err == nil {
		return
	}
	os.MkdirAll(path, 0777)
}

func GetFileName(prefixName string) string {
	curTime := time.Now()
	return fmt.Sprintf("%s.%d-%02d-%02d_%02d.log", prefixName, curTime.Year(), curTime.Month(), curTime.Day(), curTime.Hour())
}

func checkError() {
	if r := recover(); r != nil {
		buf := make([]byte, 1<<10)
		n := runtime.Stack(buf, false)
		gLogger.Errorf("%v - %s", r, buf[:n])
	} else {
		gLogger.Tracef("recover %v", r)
	}
}
